// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/okex_v5/api_order/native_private_client.h"

#include <memory>
#include <string>

#include <rapidjson/writer.h>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/okex/symbology/product.h"
#include "coin2/exchange/okex_common/api_util/rate_tracker.h"

#define CHECK_API_LIMIT(action_type, query_type, intrument)                               \
  if (!local_rate_tracker_->HasEnoughRateLimit(ts, action_type, query_type, intrument)) { \
    if (order_logger_cb_) {                                                               \
      rate_tracker_->WriteRateLimitReportMessage(order_logger_cb_);                       \
    }                                                                                     \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

using coin2::exchange::okex::symbology::OkexProduct;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;

auto OkexV5NativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  auto product = OkexProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_px(formatter.FormatPrice(order_spec.price));
  params.set_sz(formatter.FormatQty(order_spec.qty));
  params.set_cl_ord_id(GetProcOrderId(proc_order_id));

  params.set_inst_id(order_spec.product);
  // linchuan: document say "cash", but will be rejected in sim,
  // "isolated" sumbit and cancel ok, but get no query info
  params.set_td_mode("cash");

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.post_only) {
    params.set_ord_type("post_only");
  } else if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_ord_type("fok");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_ord_type("ioc");
  } else if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_ord_type("market");
  } else {
    params.set_ord_type("limit");
  }

  return params;
}

auto OkexV5NativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  params.set_inst_id(order_context.native_product());
  if (order_context.has_exchange_order_id()) {
    params.set_ord_id(order_context.exchange_order_id());
  } else {
    params.set_cl_ord_id(GetProcOrderId(order_context.proc_order_id()));
  }
  return params;
}

std::string OkexV5NativePrivateClient::GetProcOrderId(int64_t proc_order_id) {
  return "pl" + std::to_string(proc_order_id);
}

OkexV5NativePrivateClient::OkexV5NativePrivateClient(
    const MarketExchangeApi& mea,
    const NativeClientConfig& config_override) {
  mea_ = mea;
  NativeClientConfig config;
  config.set_rest_host("www.okx.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[OkexV5NativePrivateClient] config: " << config.DebugString();

  auths_.Construct<OkexRestAuth, NativeClientConfig>(config);

  rate_tracker_ = std::make_unique<OkexRateTracker>(mea_, config);
  local_rate_tracker_ = static_cast<OkexRateTracker*>(rate_tracker_.get());
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryAccountInfo() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::BALANCE, "");
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/account/balance", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::BALANCE, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryFundBalance() {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::BALANCE, "");
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/balances", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::BALANCE, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryOpenOrders(
    const std::string& inst_type,
    std::string_view after_order_id) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::OPEN_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type);
  builder.PutGet("limit", "100");
  if (!after_order_id.empty()) {
    builder.PutGet("after", after_order_id.data());
  }
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/orders-pending", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::OPEN_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryHistoryOrders(
    const std::string& inst_type) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::HIST_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type);
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/orders-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::HIST_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_DONE_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryFills(
    const std::string& inst_type) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::FILL_ORDER, "");
  UrlParamsBuilder builder;
  builder.PutGet("instType", inst_type);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/api/v5/trade/fills", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::FILL_ORDER, "");
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::PlaceSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string instrument = params.td_mode() + params.inst_id();
  CHECK_API_LIMIT(ActionType::PLACE, QueryType::UNKNOWN, instrument);
  UrlParamsBuilder builder;
  builder.PutPost("instId", params.inst_id());
  builder.PutPost("tdMode", params.td_mode());
  builder.PutPost("ordType", params.ord_type());
  builder.PutPost("side", params.side());
  if (params.has_cl_ord_id()) {
    builder.PutPost("clOrdId", params.cl_ord_id());
  }
  if (params.has_px()) {
    builder.PutPost("px", params.px());
  }
  builder.PutPost("sz", params.sz());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/api/v5/trade/order", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::PLACE, QueryType::UNKNOWN, instrument);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::PlaceBatchOrders(
    const std::vector<RestParams>& params) {
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("instId");
    writer.String(param.inst_id().c_str());
    writer.Key("tdMode");
    writer.String(param.td_mode().c_str());
    writer.Key("ordType");
    writer.String(param.ord_type().c_str());
    writer.Key("side");
    writer.String(param.side().c_str());
    writer.Key("sz");
    writer.String(param.sz().c_str());
    if (param.has_px()) {
      writer.Key("px");
      writer.String(param.px().c_str());
    }
    if (param.has_cl_ord_id()) {
      writer.Key("clOrdId");
      writer.String(param.cl_ord_id().c_str());
    }

    writer.EndObject();
  }

  writer.EndArray();

  auto* context = static_cast<OkexRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      "/api/v5/trade/batch-orders",
      str_buf.GetString());
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::CancelSingleOrder(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  std::string instrument = params.td_mode() + params.inst_id();
  CHECK_API_LIMIT(ActionType::CANCEL, QueryType::UNKNOWN, instrument);
  UrlParamsBuilder builder;
  builder.PutPost("instId", params.inst_id());
  CHECK(params.has_ord_id() != params.has_cl_ord_id())
      << "only one of order_id and client_id can be set!" << params.DebugString();

  if (params.has_ord_id() > 0) {
    builder.PutPost("ordId", params.ord_id());
  } else {
    builder.PutPost("clOrdId", params.cl_ord_id());
  }
  auto* context =
      GetAuth()->CreateRequestByPostWithSignature("/api/v5/trade/cancel-order", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::CANCEL, QueryType::UNKNOWN, instrument);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::CancelBatchOrders(
    const std::vector<RestParams>& params) {
  rapidjson::StringBuffer str_buf;
  rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
  writer.StartArray();

  for (const auto& param : params) {
    writer.StartObject();

    writer.Key("instId");
    writer.String(param.inst_id().c_str());

    if (param.has_ord_id()) {
      writer.Key("ordId");
      writer.String(param.ord_id().c_str());
    } else if (param.has_cl_ord_id()) {
      writer.Key("clOrdId");
      writer.String(param.cl_ord_id().c_str());
    } else {
      NOTREACHED() << "one of ord_id and cl_ord_id must set!";
    }

    writer.EndObject();
  }

  writer.EndArray();

  auto* context = static_cast<OkexRestAuth*>(GetAuth())->CreateRequestByPostWithSignature(
      "/api/v5/trade/cancel-batch-orders",
      str_buf.GetString());
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_place_batch_orders) {
    const auto& order_spec = *order_exe.order_spec;
    const auto proc_order_id = order_exe.proc_order_id;
    auto params = GetRestOrderParams(order_spec, proc_order_id);
    return PlaceSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->place_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 20 && !mutable_order_exe->place_pending_order_deque.empty(); ++i) {
    const auto* order_spec = mutable_order_exe->place_pending_order_deque.front();
    mutable_order_exe->place_pending_order_deque.pop_front();

    mutable_order_exe->place_proceed_order_deque.push_back(order_spec);

    params.emplace_back(GetRestOrderParams(*order_spec, order_spec->proc_order_id));
  }

  return PlaceBatchOrders(params);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  if (!order_exe.is_cancel_batch_orders) {
    auto params = GetRestCancelParams(*order_exe.oc);
    return CancelSingleOrder(params);
  }

  auto mutable_order_exe = const_cast<OrderExecution*>(&order_exe);

  if (mutable_order_exe->cancel_pending_order_deque.empty()) {
    return {};
  }

  std::vector<RestParams> params;
  for (int i = 0; i < 20 && !mutable_order_exe->cancel_pending_order_deque.empty(); ++i) {
    const auto* order_context = mutable_order_exe->cancel_pending_order_deque.front();
    mutable_order_exe->cancel_pending_order_deque.pop_front();

    mutable_order_exe->cancel_proceed_order_deque.push_back(order_context);

    RestParams param = GetRestCancelParams(*order_context);

    params.emplace_back(std::move(param));
  }

  return CancelBatchOrders(params);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryDeposit(const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::DEPOSIT, "");
  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("after", params.end_time());
    builder.PutGet("before", params.start_time());
  }

  builder.PutGet("limit", 100);
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/deposit-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::DEPOSIT, "");
  api_req_info_[__func__]++;

  context->query_type = OkexV5OrderSubTopicId::REST_DEPOSIT;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryWithdrawal(
    const RestParams& params) {
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY, QueryType::WITHDRAWAL, "");
  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("after", params.end_time());
    builder.PutGet("before", params.start_time());
  }

  builder.PutGet("limit", 100);
  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/withdrawal-history", &builder);
  local_rate_tracker_->Push(1, ts, ActionType::QUERY, QueryType::WITHDRAWAL, "");
  api_req_info_[__func__]++;

  context->query_type = OkexV5OrderSubTopicId::REST_WITHDRAWAL;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::QueryTransfer(
    const RestParams& params) {
  UrlParamsBuilder builder;
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("after", params.end_time());
    builder.PutGet("before", params.start_time());
  }
  builder.PutGet("limit", 100);

  auto* context =
      GetAuth()->CreateRequestByGetWithSignature("/api/v5/asset/subaccount/bills", &builder);
  context->query_type = OrderSubTopicId::TRANSFER;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceSingleOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelSingleOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> OkexV5NativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountInfo();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      return QueryOpenOrders(params.inst_type(), "");
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList OkexV5NativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  RestParams params;
  auto fill_func = [&params](int64_t start_time, int64_t end_time) {
    params.set_start_time(start_time / 1000'000);
    params.set_end_time(end_time / 1000'000);
  };

  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryTransfer(params));
      break;
    }

    case RestQueryProto::QUERY_WITHDRAW: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryWithdrawal(params));
      break;
    }
    case RestQueryProto::QUERY_DEPOSIT: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryDeposit(params));
      break;
    }

    default: {
      return list;
    }
  }

  return list;
}
